{% extends "admin/base.html" %}

{% block content %}
<style>
    /* 迁移 llm-performance-test.html 的样式，并适配上下布局 */
    html, body {
        padding: 0;
        box-sizing: border-box;
        background-color: #ffffff;
        font-family: "Microsoft YaHei", "微软雅黑", sans-serif;
        font-size: 16px; /* 修改为16px，与其他页面统一 */
    }
    *, *::before, *::after { box-sizing: inherit; }
    input, button, textarea, select { font-family: inherit; }
    .page-container { display: flex; flex-direction: column; gap: 20px; }
    .page-header { display: flex; flex-direction: column; justify-content: center; align-items: center; }
    .page-title { color: #000; font-size: 28px; font-weight: bold; }
    .page-desc { color: #555; font-size: 18px; }
    .input-header { padding: 8px 0; border-radius: 4px; background-color: #e6f7ff; font-size: 16px; font-weight: bold; }
    .input-field { padding: 5px 0; display: flex; align-items: center; }
    .input-field label { padding: 4px 8px 4px 0; white-space: nowrap; font-size: 15px; }
    .input-field input[type="text"], .input-field input[type="password"], .input-field input[type="number"] {
        box-sizing: border-box; padding: 5px; flex: 1; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;
    }
    #lengthTip { padding: 4px 0; color: #888; font-size: 14px; }
    .start-button, .stop-button { width: 100%; padding: 14px 20px; border: none; border-radius: 5px; color: #fff; font-size: 20px; cursor: pointer; transition: background-color 0.3s ease; }
    .start-button { background-color: #28a745; }
    .start-button:hover { background-color: #1e7e34; }
    .stop-button { background-color: #dc3545; }
    .stop-button:hover { background-color: #c82333; }
    .center-buttons { display: flex; gap: 10px; }
    .center-buttons button { min-width: 120px; padding: 10px 15px; flex-grow: 1; border: none; border-radius: 5px; background-color: #007bff; color: #fff; font-size: 15px; cursor: pointer; transition: background-color 0.3s ease; }
    .center-buttons button:hover { background-color: #0056b3; }
    .summary-display { padding: 15px; border: 1px solid #ddd; border-bottom: 1.5px solid #e0e0e0; border-radius: 8px 8px 0 0; color: #000; font-size: 15px; font-weight: 500; }
    .table-container { overflow-x: auto; flex-grow: 1; }
    .result-table { width: 100%; min-width: 500px; border-collapse: collapse; }
    .result-table th { padding: 8px; border: 1px solid #ddd; font-size: 15px; text-align: center; }
    .result-table td { padding: 8px; border: 1px solid #ddd; font-size: 15px; text-align: right; }
    .result-table th:last-child, .result-table td:last-child { min-width: 92px; text-align: center; }
    .chart-canvas { width: 1000px !important; height: 500px !important; }
    .detail-button { padding: 4px 8px; font-size: 12px; cursor: pointer; }
    .detail-overlay { width: 100vw; height: 100vh; position: fixed; top: 0; left: 0; z-index: 1000; display: flex; justify-content: center; align-items: center; background-color: rgba(0, 0, 0, 0.2); }
    .detail-dialog { min-width: 800px; max-width: 80%; max-height: 80%; overflow: auto; padding: 20px; display: flex; flex-direction: column; gap: 10px; border-radius: 8px; background-color: #fff; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); }
    .detail-header { padding: 0 0 10px 0; display: flex; justify-content: space-between; align-items: center; }
    .detail-title { color: #000; font-size: 20px; font-weight: bold; }
    .detail-close-button { padding: 10px 20px; border-radius: 4px; border: none; background-color: #dc3545; color: #fff; font-size: 15px; cursor: pointer; transition: background-color 0.3s ease; }
    .detail-close-button:hover { background-color: #c82333; }
    .detail-row { display: flex; flex-wrap: wrap; gap: 24px; align-items: center; }
    .detail-field { display: flex; align-items: center; gap: 8px; }
    .detail-field-label { padding: 6px 0; color: #000; font-size: 15px; font-weight: bold; }
    .detail-field-content { padding: 0 10px; background-color: #e9ecef; color: #000; font-size: 15px; }
    .detail-block-label { padding: 6px 0; color: #000; font-size: 15px; font-weight: bold; }
    .detail-block-content { padding: 10px 15px; border-left: 4px solid; border-right: 4px solid; border-radius: 6px; background-color: #e9ecef; color: #000; font-family: 'Courier New', monospace; font-size: 16px; line-height: 1.2; white-space: pre-wrap; }
    .system-prompt-block { border-left-color: #ffc107; border-right-color: #ffc107; }
    .user-prompt-block { border-left-color: #007bff; border-right-color: #007bff; }
    .output-block { border-left-color: #28a745; border-right-color: #28a745; }
    .alert-overlay { width: 100vw; height: 100vh; position: fixed; top: 0; left: 0; z-index: 2000; display: flex; align-items: center; justify-content: center; background-color: rgba(0, 0, 0, 0.3); }
    .alert-dialog { min-width: 400px; max-width: 40%; max-height: 40%; overflow: auto; padding: 20px; display: flex; flex-direction: column; gap: 10px; border-radius: 8px; background-color: #fff; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.18); text-align: center; }
    .alert-header { padding: 0 0 10px 0; border-bottom: 1px solid #e0e0e0; }
    .alert-title { color: #000; font-size: 20px; font-weight: bold; }
    .alert-content { padding: 10px 0; background-color: #f8f9fa; color: #000; font-size: 15px; line-height: 1.2; white-space: pre-wrap; }
    .alert-confirm-button { padding: 10px 20px; border-radius: 4px; border: none; background-color: #007bff; color: #fff; font-size: 15px; cursor: pointer; transition: background-color 0.3s ease; }
    .alert-confirm-button:hover { background-color: #0056b3; }
</style>
<div class="content-header">
    <h1>性能测试</h1>
</div>
<div class="page-container">
    <!-- <div class="page-header text-center mb-4">
        <div class="page-title">本地大语言模型推理性能测试工具</div>
        <div class="page-desc">
            用于测试本地大语言模型在不同输入长度下的推理速度，支持结果可视化与导出。
        </div>
    </div> -->
    <!-- 输入区域 -->
    <div class="row mb-4" style="display:flex;flex-wrap:wrap;gap:32px;">
        <!-- 屏蔽服务器信息表单区域 -->
        <!--
        <div class="col-md-4" style="min-width:320px;flex:1;">
            <div class="input-header mb-2">服务器信息</div>
            <div class="input-field mb-2">
                <label for="processor">处理器：</label>
                <input type="text" id="processor" value="AMD EPYC 9135 * 2" class="form-control">
            </div>
            <div class="input-field mb-2">
                <label for="mainboard">主板：</label>
                <input type="text" id="mainboard" value="MZ73-LM1-000" class="form-control">
            </div>
            <div class="input-field mb-2">
                <label for="memory">内存：</label>
                <input type="text" id="memory" value="DDR5 5600 MT/s" class="form-control">
            </div>
            <div class="input-field mb-2">
                <label for="gpu">显卡：</label>
                <input type="text" id="gpu" value="NVIDIA RTX 4090" class="form-control">
            </div>
            <div class="input-field mb-2">
                <label for="system">系统：</label>
                <input type="text" id="system" value="Ubuntu" class="form-control">
            </div>
        </div>
        -->
        <div class="col-md-4" style="min-width:320px;flex:1;">
            <div class="input-header mb-2">接口信息</div>
            <div class="input-field mb-2">
                <label for="requestUrl">请求地址：</label>
                <input type="text" id="requestUrl" value="http://localhost:8080/v1/chat/completions" class="form-control">
            </div>
            <div class="input-field mb-2">
                <label for="modelName">模型名称：</label>
                <input type="text" id="modelName" value="Deepseek-671B_Pro" class="form-control">
            </div>
            <div class="input-field mb-2">
                <label for="apiKey">请求密钥：</label>
                <input type="password" id="apiKey" placeholder="如果不需要，请留空" class="form-control">
            </div>
        </div>
        <div class="col-md-4" style="min-width:320px;flex:1;">
            <div class="input-header mb-2">测试信息</div>
            <div class="input-field mb-2">
                <label for="minInputLength">输入长度最小值：</label>
                <input type="number" id="minInputLength" value="128" class="form-control">
            </div>
            <div class="input-field mb-2">
                <label for="maxInputLength">输入长度最大值：</label>
                <input type="number" id="maxInputLength" value="4096" class="form-control">
            </div>
            <div class="input-field mb-2">
                <label for="step">输入长度步长：</label>
                <input type="number" id="step" value="2" class="form-control">
            </div>
            <div class="input-field mb-2">
                <label for="maxOutputLength">输出长度：</label>
                <input type="number" id="maxOutputLength" value="128" class="form-control">
            </div>
            <!-- 屏蔽模型结构长度字段 -->
            <!--
            <div class="input-field mb-2">
                <label for="structureLength">模型结构长度：</label>
                <input type="number" id="structureLength" value="16" placeholder="建议填写，常见值为 4" class="form-control">
            </div>
            -->
            <!-- <div id="lengthTip" style="color:#888;font-size:14px;">
                输入长度 = 系统提示词长度 + 用户提示词长度 + 模型结构长度
            </div> -->
        </div>
    </div>
    <!-- 按钮区域 -->
    <div class="row mb-3" style="display:flex;justify-content:center;">
        <div class="col-md-4" style="min-width:320px;">
            <button id="startButton" class="start-button w-100 mb-2" style="font-size:20px;">开始测试</button>
            <!-- <div class="center-buttons mb-2 d-flex justify-content-between">
                <button id="downloadChartButton" class="btn btn-primary">下载折线图</button>
                <button id="downloadCsvButton" class="btn btn-primary">下载 CSV 文件</button>
                <button id="downloadMarkdownButton" class="btn btn-primary">下载 Markdown 文件</button>
            </div> -->
        </div>
    </div>
    <!-- 结果表格 -->
    <div class="row mb-4">
        <div class="col-12">
            <div class="table-container">
                <div id="summaryDisplay" class="summary-display mb-2"></div>
                <table id="resultTable" class="table table-bordered result-table">
                    <thead>
                        <tr>
                            <th>输入长度</th>
                            <th>预填充耗时（ms）</th>
                            <th>预填充速度（token/s）</th>
                            <th>输出长度</th>
                            <th>输出耗时（ms）</th>
                            <th>输出速度（token/s）</th>
                            <th>详细信息</th>
                        </tr>
                    </thead>
                    <tbody>
                    </tbody>
                </table>
            </div>
        </div>
    </div>
    <!-- 图表区域 -->
    <div class="row">
        <div class="col-12 mb-4" style="display:flex;flex-direction:column;align-items:center;">
            <canvas id="prefillChart" class="chart-canvas mb-3" style="width:100%;max-width:1000px;height:500px;"></canvas>
            <canvas id="outputChart" class="chart-canvas" style="width:100%;max-width:1000px;height:500px;"></canvas>
        </div>
    </div>
</div>
{% endblock %}

{% block scripts %}
<script>
    // 模型服务和当前模型检测功能（与 base.html 保持一致）
    document.addEventListener('DOMContentLoaded', function() {
        const statusText = document.getElementById('model-status-text');
        const refreshStatusIcon = document.getElementById('refresh-model-status');
        const modelNameSpan = document.getElementById('current-model-name');
        const refreshModelIcon = document.getElementById('refresh-model-name');

        function checkModelStatus() {
            statusText.textContent = '加载中...';
            statusText.className = 'text-warning';

            fetch('/model_status')
                .then(response => {
                    if (response.ok) {
                        return response.text();
                    } else {
                        throw new Error('服务未响应');
                    }
                })
                .then(data => {
                    statusText.textContent = '模型服务已启动';
                    statusText.className = 'text-success';
                })
                .catch(error => {
                    statusText.textContent = '模型服务未启动';
                    statusText.className = 'text-danger';
                });
        }

        function fetchModelName() {
            modelNameSpan.textContent = '加载中...';
            modelNameSpan.className = 'text-warning';

            fetch('/current_model')
                .then(response => response.json())
                .then(data => {
                    if (data.model_name) {
                        modelNameSpan.textContent = data.model_name;
                        modelNameSpan.className = 'text-success';
                    } else {
                        throw new Error(data.error || '未知错误');
                    }
                })
                .catch(error => {
                    modelNameSpan.textContent = '无';
                    modelNameSpan.className = 'text-danger';
                    console.error('模型名称获取失败:', error);
                });
        }

        if (refreshStatusIcon) {
            refreshStatusIcon.addEventListener('click', checkModelStatus);
        }

        if (refreshModelIcon) {
            refreshModelIcon.addEventListener('click', fetchModelName);
        }

        // 页面加载时自动获取状态和模型名称
        checkModelStatus();
        fetchModelName();
    });
</script>
<script>
    const words = ['one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten'];
        let isTesting = false;
        let testStartTime = null;
        let testResults = [];
        let abortController = null;

        // 获取 DOM 元素
        const startButton = document.getElementById('startButton');
        const downloadChartButton = document.getElementById('downloadChartButton');
        const downloadCsvButton = document.getElementById('downloadCsvButton');
        const downloadMarkdownButton = document.getElementById('downloadMarkdownButton');
        const resultTableBody = document.querySelector('#resultTable tbody');
        const prefillChartCanvas = document.getElementById('prefillChart');
        const outputChartCanvas = document.getElementById('outputChart');
        const summaryDisplayEl = document.getElementById('summaryDisplay');
        const lengthTipEl = document.getElementById('lengthTip');

        startButton.addEventListener('click', () => {
            if (isTesting) {
                stopTest();
            } else {
                startTest();
            }
        });

        downloadChartButton.addEventListener('click', downloadChart);
        downloadCsvButton.addEventListener('click', downloadCsv);
        downloadMarkdownButton.addEventListener('click', downloadMarkdown);

        // 更新 summary 展示区域
        updateSummaryDisplay();

        // 实时更新 summary 展示区域
        ['processor', 'mainboard', 'memory', 'gpu', 'system', 'engineName', 'engineModelName'].forEach(id => {
            const el = document.getElementById(id);
            if (el) {
                el.addEventListener('input', updateSummaryDisplay);
            }
        });

        console.log("本工具由 chaoshen999 开发，欢迎前往 https://github.com/chaoshen999 点个 ⭐ 支持一下！");

        /**
         * 开始性能测试
         */
        async function startTest() {
            // console.log('开始测试。');

            startButton.textContent = '停止测试';
            startButton.classList.add('stop-button');
            resultTableBody.innerHTML = '';
            clearChart();

            isTesting = true;
            testStartTime = new Date();
            testResults = [];

            // 获取输入框的值
            const requestUrl = document.getElementById('requestUrl').value;
            const modelName = document.getElementById('modelName').value;
            const apiKey = document.getElementById('apiKey').value;
            const minInputLength = parseInt(document.getElementById('minInputLength').value);
            const maxInputLength = parseInt(document.getElementById('maxInputLength').value);
            const step = parseInt(document.getElementById('step').value);
            const maxOutputLength = parseInt(document.getElementById('maxOutputLength').value);
            // 屏蔽结构长度字段，直接赋值为 0
            // const structureLengthInput = document.getElementById('structureLength');
            // const structureLength = structureLengthInput.value === '' ? 0 : parseInt(structureLengthInput.value);
            const structureLength = 0;

            // 校验输入的值
            if (!requestUrl || !modelName || isNaN(minInputLength) || isNaN(maxInputLength) || isNaN(step) || isNaN(maxOutputLength) /* || isNaN(structureLength) */) {
                showAlert('请填写接口信息和测试信息。');
                stopTest();
                return;
            }
            if (minInputLength <= 0 || maxInputLength < minInputLength || step <= 0 || maxOutputLength <= 0 /* || structureLength < 0 */) {
                showAlert('请填写合理的测试信息。');
                stopTest();
                return;
            }

            if (minInputLength < 20) {
                showAlert('输入长度最小值需要大于等于 20。');
                stopTest();
                return;
            }

            if (maxOutputLength < 3) {
                showAlert('输出长度需要大于等于 3。');
                stopTest();
                return;
            }

            // 在开始测试前，建立 HTTP 链路
            try {
                const systemPrompt = generateSystemPrompt();
                const userPrompt = generateUserPrompt(10);
                const headers = {
                    'Content-Type': 'application/json'
                };
                if (apiKey) {
                    headers['Authorization'] = `Bearer ${apiKey}`;
                }
                const requestBody = JSON.stringify({
                    model: modelName,
                    messages: [
                        { role: 'system', content: systemPrompt },
                        { role: 'user', content: userPrompt }
                    ],
                    max_tokens: 3,
                    temperature: 0.1,
                    top_p: 0.1,
                    stream: true
                });
                const response = await fetch(requestUrl, {
                    method: 'POST',
                    headers: headers,
                    body: requestBody
                });
                if (response.body && response.body.getReader) {
                    const reader = response.body.getReader();
                    while (true) {
                        const { done } = await reader.read();
                        if (done) break;
                    }
                }
                await new Promise(resolve => setTimeout(resolve, 1000));
            } catch (e) {
                // 忽略错误
            }

            // 批量测试
            for (let inputLength = minInputLength; inputLength <= maxInputLength; inputLength *= step) {
                console.log('>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>');
                console.log(logTime(), '输入长度：', inputLength);

                if (!isTesting) {
                    return;
                }

                let testRowData = {};
                let serverCompletionTokens = 0;
                let streamTokens = 0;
                let output = '';

                let requestTime = undefined;
                let responseTime = undefined;
                let firstTokenTime = undefined;
                let outputFinishTime = undefined;

                let prefillTime = undefined;
                let prefillSpeed = undefined;
                let outputLength = undefined;
                let outputTime = undefined;
                let outputSpeed = undefined;

                // 生成系统提示词
                const systemPrompt = generateSystemPrompt();
                const systemPromptLength = countTokensByRule(systemPrompt);
                // console.log('系统提示词内容：', systemPrompt);
                // console.log('系统提示词长度：', systemPromptLength);

                // 生成用户提示词
                // 用户提示词长度 = 输入长度 - 系统提示词长度 - 大模型结构占用长度
                const userPromptLength = inputLength - systemPromptLength - structureLength;
                const userPrompt = generateUserPrompt(userPromptLength);
                // console.log('用户提示词内容：', userPrompt);
                // console.log('用户提示词长度：', userPromptLength);

                try {
                    // 请求头
                    const headers = {
                        'Content-Type': 'application/json'
                    };
                    if (apiKey) {
                        headers['Authorization'] = `Bearer ${apiKey}`;
                    }
                    // 请求体
                    const requestBody = JSON.stringify({
                        model: modelName,
                        messages: [
                            { role: 'system', content: systemPrompt },
                            { role: 'user', content: userPrompt }
                        ],
                        max_tokens: maxOutputLength,
                        temperature: 1,
                        top_p: 0.1,
                        stream: true
                    });

                    // 初始化 AbortController
                    abortController = new AbortController();
                    const signal = abortController.signal;

                    requestTime = performance.now();
                    console.log(logTime(), '发送请求的时间：', formatTime(requestTime));

                    const response = await fetch(requestUrl, {
                        method: 'POST',
                        headers: headers,
                        body: requestBody,
                        signal: signal
                    });

                    if (!response.ok) {
                        throw new Error(`HTTP 请求发生错误，状态码：${response.status}`);
                    }

                    const reader = response.body.getReader();
                    const decoder = new TextDecoder('utf-8');
                    let buffer = '';

                    while (true) {
                        console.log('==================================================');

                        const { done, value } = await reader.read();

                        responseTime = performance.now();
                        console.log(logTime(), '接收响应的时间：', formatTime(responseTime));
                        console.log(logTime(), '数据流状态：', { done, hasValue: !!value, valueLength: value?.length });

                        if (done) {
                            console.log(logTime(), 'done');

                            if (!outputFinishTime) {
                                outputFinishTime = responseTime;
                                console.log(logTime(), '输出结束的时间：', formatTime(outputFinishTime));
                            }
                            break;
                        }

                        buffer += decoder.decode(value, { stream: true });
                        const lines = buffer.split('\n');
                        buffer = lines.pop();

                        for (const line of lines) {
                            console.log(logTime(), '数据流内容：', line);

                            if (line.trim() === '') {
                                continue;
                            }
                            if (!line.startsWith('data:')) {
                                continue;
                            }

                            const data = line.substring(5).trim();
                            if (data === '[DONE]') {
                                console.log(logTime(), '[DONE]');

                                outputFinishTime = responseTime;
                                console.log(logTime(), '输出结束的时间：', formatTime(outputFinishTime));
                                break;
                            }

                            const json = JSON.parse(data);
                            if (json.choices && json.choices[0] && json.choices[0].delta) {
                                // console.log(logTime(), 'delta：', json.choices[0].delta);

                                if (json.choices[0].delta.content) {
                                    output += json.choices[0].delta.content;
                                    streamTokens++;

                                    // console.log(logTime(), 'content：', streamTokens, json.choices[0].delta.content);

                                    if (!firstTokenTime) {
                                        firstTokenTime = responseTime;
                                        console.log(logTime(), '接收到首字的时间：', formatTime(firstTokenTime));
                                    }
                                }
                            }
                            if (json.usage) {
                                // console.log(logTime(), 'usage：', json.usage);

                                if (json.usage.completion_tokens) {
                                    serverCompletionTokens = json.usage.completion_tokens;
                                }
                            }
                        }
                    }

                    // 预填充耗时：从发送请求到接收到首字经过的时间
                    prefillTime = firstTokenTime - requestTime;
                    // 输出耗时：从接收到首字到输出结束经过的时间
                    outputTime = outputFinishTime - firstTokenTime;

                    // 获取输出长度，优先使用服务器接口返回的 token 数；
                    //     其次使用流输出的 token 数（可能不准确，因为一次流可能包含多个 token）；
                    //     最后统计已接收的输出内容计算 token 数
                    outputLength = 0;
                    if (serverCompletionTokens > 0) {
                        outputLength = serverCompletionTokens;
                    } else if (streamTokens > 0) {
                        outputLength = streamTokens;
                    } else {
                        outputLength = countTokensByRule(output);
                    }
                    // console.log('输出内容：', output);
                    // console.log('输出长度：', outputLength);

                    // 计算预填充速度和输出速度，保留两位小数
                    prefillSpeed = prefillTime > 0 ? (inputLength / (prefillTime / 1000)).toFixed(2) : 'N/A';
                    outputSpeed = outputTime > 0 ? (outputLength / (outputTime / 1000)).toFixed(2) : 'N/A';

                    testRowData = {
                        inputLength: inputLength,
                        prefillTime: prefillTime.toFixed(2),
                        prefillSpeed: prefillSpeed,
                        outputLength: outputLength,
                        outputTime: outputTime.toFixed(2),
                        outputSpeed: outputSpeed,
                        systemPrompt: systemPrompt,
                        systemPromptLength: systemPromptLength,
                        userPrompt: userPrompt,
                        userPromptLength: userPromptLength,
                        output: output,
                        error: false
                    };
                } catch (error) {
                    if (error.name === 'AbortError') {
                        console.error(logTime(), error);

                    } else {
                        console.error(logTime(), error);

                        // 根据错误类型提供更详细的错误信息
                        let errorMessage = '未知错误';
                        if (error.name === 'TypeError' && error.message.includes('fetch')) {
                            errorMessage = '网络连接失败，请检查服务器地址和网络连接';
                        } else if (error.message.includes('Failed to fetch')) {
                            errorMessage = '无法连接到服务器，请检查服务器是否运行';
                        } else if (error.message.includes('Unexpected token')) {
                            errorMessage = '服务器返回的数据格式错误';
                        } else if (error.message.includes('timeout')) {
                            errorMessage = '请求超时，请检查服务器响应时间';
                        }

                        testRowData = {
                            inputLength: inputLength,
                            prefillTime: 0,
                            prefillSpeed: 0,
                            outputLength: 0,
                            outputTime: 0,
                            outputSpeed: 0,
                            systemPrompt: systemPrompt || '',
                            systemPromptLength: systemPromptLength || 0,
                            userPrompt: userPrompt || '',
                            userPromptLength: userPromptLength || 0,
                            output: '',
                            error: true,
                            errorMessage: errorMessage
                        };
                    }
                }

                // 在表格中显示数据
                const row = resultTableBody.insertRow();
                row.insertCell().textContent = testRowData.inputLength;
                row.insertCell().textContent = testRowData.error ? '错误' : testRowData.prefillTime;
                row.insertCell().textContent = testRowData.error ? '错误' : testRowData.prefillSpeed;
                row.insertCell().textContent = testRowData.error ? '错误' : testRowData.outputLength;
                row.insertCell().textContent = testRowData.error ? '错误' : testRowData.outputTime;
                row.insertCell().textContent = testRowData.error ? '错误' : testRowData.outputSpeed;

                // 添加详细信息按钮
                const detailCell = row.insertCell();
                const detailButton = document.createElement('button');
                detailButton.textContent = '详细信息';
                detailButton.className = 'detail-button';
                detailButton.onclick = () => showDetail(
                    testRowData.inputLength,
                    testRowData.systemPrompt,
                    testRowData.systemPromptLength,
                    testRowData.userPrompt,
                    testRowData.userPromptLength,
                    testRowData.output,
                    testRowData.outputLength
                );
                detailCell.appendChild(detailButton);

                // 存储结果
                testResults.push(testRowData);

                // 每次请求结束后重画一次折线图
                drawChart();

                // 每次请求结束后等待 1000 ms
                await new Promise(resolve => setTimeout(resolve, 1000));
            }

            // 批量测试完成后，重置按钮状态
            stopTest();
        }

        /**
         * 停止性能测试
         */
        function stopTest() {
            // console.log('停止测试。');

            isTesting = false;
            if (abortController) {
                abortController.abort();
                abortController = null;
            }
            startButton.textContent = '开始测试';
            startButton.classList.remove('stop-button');
        }

        function logTime() {
            return `\x1b[34m[${new Date().toISOString()}]\x1b[0m`;
        }

        function formatTime(time) {
            const fixed = time.toFixed(3);
            const [intPart, fracPart] = fixed.split('.');
            const intPartPadded = intPart.padStart(10, ' ');
            return `${intPartPadded}.${fracPart}`;
        }

        /**
         * 按规则分词，返回 token 数量
         * @param {string} text - 要分词的文本
         * @returns {number} token 数量
         */
        function countTokensByRule(text) {
            // 1. 识别带撇号的缩写词
            const specialApostropheWords = [
                "'cause", "'em", "'kay", "'nother", "'round", "'scuse", "'sup", "'til"
            ];

            // 2. 识别需要计数 1 个 token 的缩写词
            const oneTokenWords = [
                "don't", "can't", "won't"
            ];

            // 3. 识别需要计数 2 个 token 的缩写词
            const twoTokenWords = [
                // 'm 系列
                "I'm",
                // 're 系列
                "we're", "you're", "they're",
                // 've 系列
                "I've", "we've", "you've", "they've",
                // 'd 系列
                "I'd", "we'd", "you'd", "he'd", "she'd", "they'd",
                // 'll 系列
                "I'll", "we'll", "you'll", "he'll", "she'll", "they'll",
                // n't 系列
                "isn't", "aren't", "wasn't", "weren't", "doesn't", "didn't", "hasn't", "haven't", "shouldn't", "wouldn't", "couldn't",
                // 's 系列
                "it's", "he's", "she's", "that's", "there's", "who's"
            ];

            let tokens = [];
            let t = text;

            specialApostropheWords.forEach(word => {
                let reg = new RegExp(word.replace("'", "\\'"), 'gi');
                t = t.replace(reg, m => {
                    tokens.push(m);
                    return ' '.repeat(m.length);
                });
            });

            oneTokenWords.forEach(word => {
                let reg = new RegExp(word.replace("'", "\\'"), 'gi');
                t = t.replace(reg, m => {
                    tokens.push(m);
                    return ' '.repeat(m.length);
                });
            });

            twoTokenWords.forEach(word => {
                let reg = new RegExp(word.replace("'", "\\'"), 'gi');
                t = t.replace(reg, m => {
                    const parts = m.split(/'/);
                    tokens.push(parts[0]);
                    tokens.push("'" + parts[1]);
                    return ' '.repeat(m.length);
                });
            });

            // 4. 对剩余内容使用正常规则统计
            const enPunct = /[,.!?;:'"()\[\]{}]/;
            const cnPunct = /[，。！？；：‘’“”（）【】｛｝]/;

            const isSeparator = (ch) => ch === ' ' || enPunct.test(ch) || cnPunct.test(ch) || ch === '\n' || ch === '\t' || ch === '\r';
            const isDigit = (ch) => /[0-9]/.test(ch);

            let buffer = '';
            let i = 0;
            while (i < t.length) {
                const ch = t[i];

                // 如果是分隔符
                if (isSeparator(ch)) {
                    // 如果前面有 buffer，先将 buffer 加入 tokens
                    if (buffer.length > 0) {
                        tokens.push(buffer);
                        buffer = '';
                    }
                    // 分隔符本身算一个 token（空格，制表符，回车符除外）
                    if (ch !== ' ' && ch !== '\t' && ch !== '\r') {
                        tokens.push(ch);
                    }
                    i++;
                    continue;
                }

                // 如果是数字
                if (isDigit(ch)) {
                    // 如果前面有 buffer，先将 buffer 加入 tokens
                    if (buffer.length > 0) {
                        tokens.push(buffer);
                        buffer = '';
                    }
                    // 数字本身算一个 token
                    tokens.push(ch);
                    i++;
                    continue;
                }

                // 其他字符拼接到 buffer
                buffer += ch;
                i++;
            }

            // 最后一个单词，加入 tokens
            if (buffer.length > 0) {
                tokens.push(buffer);
            }

            // 过滤掉空字符串
            tokens = tokens.filter(t => t && t.length > 0);
            return tokens.length;
        }

        /**
         * 生成系统提示词
         * @returns {string} 系统提示词内容
         */
        function generateSystemPrompt() {
            return 'You are a assistant.';
        }

        /**
         * 生成用户提示词
         * @param {number} length - 长度
         * @returns {string} 用户提示词内容
         */
        function generateUserPrompt(length) {
            const fixedPrompt = `\nRepeat the above content one hundred times.`;
            const fixedPromptLength = countTokensByRule(fixedPrompt);

            const needWordLength = length - fixedPromptLength;
            let userPrompt = '';
            for (let i = 0; i < needWordLength; i++) {
                const randomWord = words[Math.floor(Math.random() * words.length)];
                if (i > 0) {
                    userPrompt += ' ';
                }
                userPrompt += randomWord;
            }
            userPrompt += fixedPrompt;
            return userPrompt;
        }

        /**
         * 绘制性能测试结果的折线图
         */
        function drawChart() {
            const inputLengths = testResults.map(r => r.inputLength);
            const prefillSpeeds = testResults.map(r => r.prefillSpeed);
            const outputSpeeds = testResults.map(r => r.outputSpeed);

            drawLineChart(prefillChartCanvas, inputLengths, prefillSpeeds, '预填充速度', '输入长度', '预填充速度（token/s）', '#4bc0c0');
            drawLineChart(outputChartCanvas, inputLengths, outputSpeeds, '输出速度', '输入长度', '输出速度（token/s）', '#ff6384');
        }

        /**
         * 用原生 canvas 绘制折线图
         * @param {HTMLCanvasElement} canvas - 画布
         * @param {Array} xData - X 轴数据
         * @param {Array} yData - Y 轴数据
         * @param {string} title - 图表标题
         * @param {string} xLabel - X 轴标题
         * @param {string} yLabel - Y 轴标题
         * @param {string} lineColor - 折线颜色
         */
        function drawLineChart(canvas, xData, yData, title, xLabel, yLabel, lineColor) {
            // 固定画布为 1000*500
            canvas.width = 1000;
            canvas.height = 500;

            const cssWidth = 1000;
            const cssHeight = 500;
            const ctx = canvas.getContext('2d');
            ctx.setTransform(1, 0, 0, 1, 0, 0);

            // 间距
            const titleTop = 15; // 标题上边距
            const chartTop = 80; // 折线图本身上边距
            const chartBottom = 50; // 折线图本身下边距
            const chartLeft = 100; // 折线图本身左边距
            const chartRight = 100; // 折线图本身右边距
            const xLabelOffset = 15; // X 轴标题距离折线图 X 轴右端
            const yLabelOffset = 15; // Y 轴标题距离折线图 Y 轴上端

            // 字体字号
            const titleFont = 20; // 标题字体
            const xTickFont = 16; // X 轴刻度数字字体
            const yTickFont = 15; // Y 轴刻度数字字体
            const xLabelFont = 16; // X 轴标题字体
            const yLabelFont = 16; // Y 轴标题字体
            const dataPointFont = 15; // 数据点数值字体

            // 刻度
            const yTickCount = 5; // Y 轴刻度数量

            // 图表区范围
            const chartWidth = cssWidth - chartLeft - chartRight;
            const chartHeight = cssHeight - chartTop - chartBottom;

            // X 轴添加 0 刻度，生成 X 轴数组
            const chartXStart = chartLeft;
            const chartXEnd = chartLeft + chartWidth;
            const xTicks = [0, ...xData];
            const xTickCount = xTicks.length;

            function getX(i) {
                return chartXStart + (chartWidth / (xTickCount - 1)) * i;
            }

            // Y 轴添加 0 刻度，生成 Y 轴数组
            const chartYStart = chartTop + chartHeight;
            const chartYEnd = chartTop;
            let yMax = Math.max(...yData);
            yMax = getNiceMaxValue(yMax);

            const yTicks = [];
            for (let i = 0; i < yTickCount; i++) {
                yTicks.push((yMax / (yTickCount - 1)) * i);
            }

            //获取大于等于 max 的最接近的整数量级（如 1、2、5、10、20、50、100、200、500、1000 等）
            function getNiceMaxValue(max) {
                if (max <= 0) {
                    return 1
                };

                const exp = Math.floor(Math.log10(max));
                const base = Math.pow(10, exp);
                let niceMax = base;
                if (max > base) {
                    // 取 1，2，5，10 倍的 base
                    if (max <= 2 * base) {
                        niceMax = 2 * base;
                    } else if (max <= 5 * base) {
                        niceMax = 5 * base;
                    } else {
                        niceMax = 10 * base;
                    }
                }
                return niceMax;
            }

            function getY(i) {
                return chartYStart - (chartHeight / (yTickCount - 1)) * i;
            }

            function getYByValue(value) {
                return chartYStart - (value / yMax) * chartHeight;
            }

            // 背景
            ctx.fillStyle = '#fff';
            ctx.fillRect(0, 0, cssWidth, cssHeight);

            // 标题
            ctx.font = `bold ${titleFont}px "Microsoft YaHei", "微软雅黑", sans-serif`;
            ctx.fillStyle = '#000';
            ctx.textAlign = 'center';
            ctx.textBaseline = 'top';
            ctx.fillText(title, cssWidth / 2, titleTop);

            // 画网格线
            ctx.strokeStyle = '#e0e0e0';
            ctx.lineWidth = 1;
            for (let i = 0; i < xTickCount; i++) {
                const x = getX(i);
                ctx.beginPath();
                ctx.moveTo(x, chartYStart);
                ctx.lineTo(x, chartYEnd);
                ctx.stroke();
            }
            for (let i = 0; i < yTickCount; i++) {
                const y = getY(i);
                ctx.beginPath();
                ctx.moveTo(chartXStart, y);
                ctx.lineTo(chartXEnd, y);
                ctx.stroke();
            }

            // 画坐标轴
            ctx.strokeStyle = '#000';
            ctx.lineWidth = 2;
            // X 轴
            ctx.beginPath();
            ctx.moveTo(chartXStart, chartYStart);
            ctx.lineTo(chartXEnd, chartYStart);
            ctx.stroke();
            // Y 轴
            ctx.beginPath();
            ctx.moveTo(chartXStart, chartYStart);
            ctx.lineTo(chartXStart, chartYEnd);
            ctx.stroke();

            // 画 X 轴刻度和数字
            ctx.font = `${xTickFont}px "Microsoft YaHei", "微软雅黑", sans-serif`;
            ctx.fillStyle = '#000';
            ctx.textAlign = 'center';
            ctx.textBaseline = 'top';
            for (let i = 0; i < xTickCount; i++) {
                const x = getX(i);
                ctx.beginPath();
                ctx.moveTo(x, chartYStart);
                ctx.lineTo(x, chartYStart + 8);
                ctx.stroke();
                ctx.fillText(xTicks[i], x, chartYStart + 12);
            }

            // 画 Y 轴刻度和数字
            ctx.font = `${yTickFont}px "Microsoft YaHei", "微软雅黑", sans-serif`;
            ctx.fillStyle = '#000';
            ctx.textAlign = 'right';
            ctx.textBaseline = 'middle';
            for (let i = 0; i < yTickCount; i++) {
                const y = getY(i);
                ctx.beginPath();
                ctx.moveTo(chartXStart - 8, y);
                ctx.lineTo(chartXStart, y);
                ctx.stroke();
                ctx.fillText(yTicks[i].toFixed(2), chartXStart - 12, y);
            }

            // X 轴标题，左边界离折线图 X 轴右端 15 px
            ctx.font = `bold ${xLabelFont}px "Microsoft YaHei", "微软雅黑", sans-serif`;
            ctx.fillStyle = '#000';
            ctx.textAlign = 'left';
            ctx.textBaseline = 'middle';
            ctx.fillText(xLabel, chartXEnd + xLabelOffset, chartYStart);

            // Y 轴标题，下边界离折线图 Y 轴上端 15 px
            ctx.font = `bold ${yLabelFont}px "Microsoft YaHei", "微软雅黑", sans-serif`;
            ctx.fillStyle = '#000';
            ctx.textAlign = 'center';
            ctx.textBaseline = 'top';
            ctx.fillText(yLabel, chartXStart, chartYEnd - yLabelOffset - yLabelFont);

            // 画折线
            ctx.strokeStyle = lineColor;
            ctx.lineWidth = 3;
            ctx.beginPath();
            for (let i = 0; i < xData.length; i++) {
                const x = getX(i + 1);
                const y = getYByValue(yData[i]);
                if (i === 0) {
                    ctx.moveTo(x, y);
                } else {
                    ctx.lineTo(x, y);
                }
            }
            ctx.stroke();

            // 画数据点和数值
            for (let i = 0; i < xData.length; i++) {
                const x = getX(i + 1);
                const y = getYByValue(yData[i]);

                // 阴影
                ctx.save();
                ctx.shadowColor = 'rgba(0,0,0,0.15)';
                ctx.shadowBlur = 6;
                ctx.fillStyle = '#fff';
                ctx.beginPath();
                ctx.arc(x, y, 6, 0, 2 * Math.PI);
                ctx.fill();
                ctx.restore();

                // 外圈
                ctx.strokeStyle = lineColor;
                ctx.lineWidth = 2;
                ctx.beginPath();
                ctx.arc(x, y, 6, 0, 2 * Math.PI);
                ctx.stroke();

                // 内圈
                ctx.fillStyle = lineColor;
                ctx.beginPath();
                ctx.arc(x, y, 3, 0, 2 * Math.PI);
                ctx.fill();

                // 数值
                ctx.font = `${dataPointFont}px "Microsoft YaHei", "微软雅黑", sans-serif`;
                ctx.fillStyle = '#000';
                ctx.textAlign = 'center';
                ctx.textBaseline = 'bottom';
                let value = yData[i];
                let valueText = (typeof value === 'number')
                    ? value.toFixed(2)
                    : (typeof value === 'string' && !isNaN(Number(value)))
                        ? Number(value).toFixed(2)
                        : value;
                ctx.fillText(valueText, x, y - 10);
            }
        }

        /**
         * 清除折线图
         */
        function clearChart() {
            [prefillChartCanvas, outputChartCanvas].forEach(canvas => {
                const ctx = canvas.getContext('2d');
                ctx.clearRect(0, 0, canvas.width, canvas.height);
            });
        }

        /**
         * 下载折线图
         */
        function downloadChart() {
            if (!checkTestData('下载')) return;

            // 获取标题和 summary 信息
            const title = getTitle();
            const summaryInfoText = getSummaryInfoText();
            const testStartTimeText = getTestStartTimeText();

            // 获取两个原始 canvas
            const prefillCanvas = prefillChartCanvas;
            const outputCanvas = outputChartCanvas;

            // 计算标题和 summary 信息区域高度
            const titleFontSize = 36;
            const titleLineHeight = 36;
            const titlePadding = 24;

            const infoFontSize = 15;
            const infoLineHeight = 30;
            const infoPadding = 20;

            const titleHeight = titlePadding + titleLineHeight + titlePadding;
            const infoHeight = infoPadding + summaryInfoText.length * infoLineHeight + infoPadding;
            const timeHeight = testStartTimeText.length * infoLineHeight;
            const headHeight = titleHeight + infoHeight + timeHeight;

            // 计算合成图片的宽高
            const width = Math.max(prefillCanvas.width, outputCanvas.width);
            const height = headHeight + prefillCanvas.height + outputCanvas.height;

            // 创建合成 canvas
            const combinedCanvas = document.createElement('canvas');
            combinedCanvas.width = width;
            combinedCanvas.height = height;
            const ctx = combinedCanvas.getContext('2d');

            // 填充白色背景
            ctx.fillStyle = '#fff';
            ctx.fillRect(0, 0, width, height);

            // 绘制标题
            ctx.font = `bold ${titleFontSize}px "Microsoft YaHei", "微软雅黑", sans-serif`;
            ctx.fillStyle = '#000';
            ctx.textAlign = 'center';
            ctx.textBaseline = 'top';
            ctx.fillText(title, width / 2, titlePadding);

            // 绘制 summary 信息
            ctx.font = `bold ${infoFontSize}px "Microsoft YaHei", "微软雅黑", sans-serif`;
            ctx.fillStyle = '#000';
            ctx.textAlign = 'left';
            ctx.textBaseline = 'top';
            let y = titleHeight + infoPadding;
            summaryInfoText.forEach(line => {
                ctx.fillText(line, infoPadding, y);
                y += infoLineHeight;
            });

            // 绘制测试时间信息
            testStartTimeText.forEach(line => {
                ctx.fillText(line, infoPadding, y);
                y += infoLineHeight;
            });

            // 绘制两个原始 canvas
            ctx.drawImage(prefillCanvas, 0, headHeight);
            ctx.drawImage(outputCanvas, 0, headHeight + prefillCanvas.height);

            const imageDataUrl = combinedCanvas.toDataURL('image/png');
            const baseName = getExportBaseName();
            downloadFile(imageDataUrl, `${baseName}.png`, 'image/png');
        }

        /**
         * 下载 CSV 格式的文件
         */
        function downloadCsv() {
            if (!checkTestData('下载')) return;

            let csvContent = getTestReportHeader();

            csvContent += '\n' + getCsvColumnHeader() + '\n';

            testResults.forEach(row => {
                csvContent += `${row.inputLength},${row.prefillTime},${row.prefillSpeed},${row.outputLength},${row.outputTime},${row.outputSpeed}\n`;
            });

            const baseName = getExportBaseName();
            downloadFile(csvContent, `${baseName}.csv`, 'text/csv');
        }

        /**
         * 下载 Markdown 格式的文件
         */
        function downloadMarkdown() {
            if (!checkTestData('下载')) return;

            let markdownContent = getTestReportHeader();

            markdownContent += '\n' + getMarkdownColumnHeader() + '\n';
            markdownContent += '|---:|---:|---:|---:|---:|---:|\n';

            testResults.forEach(row => {
                markdownContent += `| ${row.inputLength} | ${row.prefillTime} | ${row.prefillSpeed} | ${row.outputLength} | ${row.outputTime} | ${row.outputSpeed} |\n`;
            });

            const baseName = getExportBaseName();
            downloadFile(markdownContent, `${baseName}.md`, 'text/markdown');
        }

        // 检查是否有测试数据
        function checkTestData(operationName) {
            if (testResults.length === 0) {
                showAlert(`没有数据可以${operationName}。`);
                return false;
            }
            return true;
        }

        // 获取 header 信息
        function getTestReportHeader() {
            let header = getTitle() + '\n\n';

            getSummaryInfoText().forEach(line => {
                header += line + '\n';
            });
            getTestStartTimeText().forEach(line => {
                header += line + '\n';
            });

            return header;
        }

        // 获取标题
        function getTitle() {
            return '本地大语言模型推理性能测试结果';
        }

        // 获取 summary 信息
        function getSummaryInfoText() {
            // const fields = ['processor', 'mainboard', 'memory', 'gpu', 'system', 'engineName', 'engineModelName'];
            const fields = ['engineName', 'engineModelName'];
            return fields.map(fieldId => {
                const label = getFieldLabel(fieldId);
                const value = getFieldValue(fieldId);
                return `${label}${value}`;
            });
        }

        // 获取测试时间信息
        function getTestStartTimeText() {
            const year = testStartTime.getFullYear();
            const month = String(testStartTime.getMonth() + 1).padStart(2, '0');
            const day = String(testStartTime.getDate()).padStart(2, '0');
            const hours = String(testStartTime.getHours()).padStart(2, '0');
            const minutes = String(testStartTime.getMinutes()).padStart(2, '0');
            const seconds = String(testStartTime.getSeconds()).padStart(2, '0');
            return [`测试时间：${year} 年 ${month} 月 ${day} 日 ${hours} 时 ${minutes} 分 ${seconds} 秒`];
        }

        // 获取导出文件的基础名（显卡,引擎名称,模型名称,测试开始时间）
        function getExportBaseName() {
            // const gpu = getFieldValue('gpu', 'gpu');
            const gpu = '无';
            const engineName = getFieldValue('engineName', 'engine');
            const engineModelName = getFieldValue('engineModelName', 'model');

            const safe = s => s.replace(/\s+/g, '_').replace(/[^\w\-\.]/g, '');

            let timeText = '';
            if (testStartTime) {
                const year = testStartTime.getFullYear();
                const month = String(testStartTime.getMonth() + 1).padStart(2, '0');
                const day = String(testStartTime.getDate()).padStart(2, '0');
                const hours = String(testStartTime.getHours()).padStart(2, '0');
                const minutes = String(testStartTime.getMinutes()).padStart(2, '0');
                const seconds = String(testStartTime.getSeconds()).padStart(2, '0');
                timeText = `${year}${month}${day}_${hours}${minutes}${seconds}`;
            }

            return `${gpu},${safe(engineName)},${safe(engineModelName)},${timeText}`;
        }

        // 获取 CSV 表格列标题
        function getCsvColumnHeader() {
            return '输入长度,预填充耗时（ms）,预填充速度（token/s）,输出长度,输出耗时（ms）,输出速度（token/s）';
        }

        // 获取 Markdown 表格列标题
        function getMarkdownColumnHeader() {
            return '| 输入长度 | 预填充耗时（ms） | 预填充速度（token/s） | 输出长度 | 输出耗时（ms） | 输出速度（token/s） |';
        }

        // 更新 summary 展示区域
        function updateSummaryDisplay() {
            // const serverFields = ['processor', 'mainboard', 'memory', 'gpu', 'system'];
            const engineFields = ['engineName', 'engineModelName'];

            // 拼接推理引擎信息
            const engineInfo = engineFields.map(fieldId => {
                const label = getFieldLabel(fieldId);
                const value = getFieldValue(fieldId);
                return `${label}${value}`;
            }).join('，') + '。';

            summaryDisplayEl.innerHTML = `<div>${engineInfo}</div>`;
        }

        // 获取表单字段标签文本
        function getFieldLabel(fieldId) {
            return document.querySelector(`label[for="${fieldId}"]`).textContent;
        }

        // 获取表单字段值，如果为空则返回默认值
        function getFieldValue(fieldId, defaultValue = '无') {
            return document.getElementById(fieldId).value || defaultValue;
        }

        // 通用文件下载函数
        function downloadFile(content, filename, mimeType) {
            const link = document.createElement('a');

            if (content.startsWith('data:')) {
                link.href = content;
            } else {
                link.href = `data:${mimeType};charset=utf-8,` + encodeURIComponent(content);
            }

            link.download = filename;
            document.body.appendChild(link);
            link.click();
            document.body.removeChild(link);
        }

        /**
         * 显示详情弹窗
         * @param {number} inputLength - 输入长度
         * @param {string} systemPrompt - 系统提示词
         * @param {number} systemPromptLength - 系统提示词长度
         * @param {string} userPrompt - 用户提示词
         * @param {number} userPromptLength - 用户提示词长度
         * @param {string} output - 接收到的输出内容
         * @param {number} outputLength - 接收到的输出长度
         */
        function showDetail(inputLength, systemPrompt, systemPromptLength, userPrompt, userPromptLength, output, outputLength) {
            // 阻止背景页面滚动
            document.body.style.overflow = 'hidden';

            const detailOverlay = document.createElement('div');
            detailOverlay.className = 'detail-overlay';

            const detailDialog = document.createElement('div');
            detailDialog.className = 'detail-dialog';

            detailDialog.innerHTML = `
                <div class="detail-header">
                    <div class="detail-title">详细信息</div>
                    <button type="button" class="detail-close-button">关闭</button>
                </div>
                <div class="detail-row">
                  <div class="detail-field">
                    <div class="detail-field-label">输入长度：</div>
                    <span class="detail-field-content">${inputLength}</span>
                  </div>
                  <div class="detail-field">
                    <div class="detail-field-label">系统提示词长度：</div>
                    <span class="detail-field-content">${systemPromptLength}</span>
                  </div>
                  <div class="detail-field">
                    <div class="detail-field-label">用户提示词长度：</div>
                    <span class="detail-field-content">${userPromptLength}</span>
                  </div>
                  <div class="detail-field">
                    <div class="detail-field-label">接收到的输出长度：</div>
                    <span class="detail-field-content">${outputLength}</span>
                  </div>
                </div>
                <div class="detail-block">
                    <div class="detail-block-label">系统提示词：</div>
                    <div class="detail-block-content system-prompt-block">${systemPrompt.replace(/</g, '&lt;').replace(/>/g, '&gt;')}</div>
                </div>
                <div class="detail-block">
                    <div class="detail-block-label">用户提示词：</div>
                    <div class="detail-block-content user-prompt-block">${userPrompt.replace(/</g, '&lt;').replace(/>/g, '&gt;')}</div>
                </div>
                <div class="detail-block">
                    <div class="detail-block-label">接收到的输出内容：</div>
                    <div class="detail-block-content output-block">${output.replace(/</g, '&lt;').replace(/>/g, '&gt;')}</div>
                </div>
            `;

            detailOverlay.appendChild(detailDialog);
            document.body.appendChild(detailOverlay);

            // 点击关闭按钮可以关闭弹窗
            const detailCloseBtn = detailDialog.querySelector('.detail-close-button');
            detailCloseBtn.addEventListener('click', () => {
                detailOverlay.remove();
                document.body.style.overflow = '';
            });

            // 点击背景可以关闭弹窗
            detailOverlay.addEventListener('click', (e) => {
                if (e.target === detailOverlay) {
                    detailOverlay.remove();
                    document.body.style.overflow = '';
                }
            });
        }

        /**
         * 显示自定义提示弹窗
         * @param {string} message - 提示内容
         * @param {string} [title='提示'] - 弹窗标题
         */
        function showAlert(message, title = '提示') {
            if (document.querySelector('.alert-overlay')) {
                return
            };

            // 阻止背景页面滚动
            document.body.style.overflow = 'hidden';

            const alertOverlay = document.createElement('div');
            alertOverlay.className = 'alert-overlay';

            const alertDialog = document.createElement('div');
            alertDialog.className = 'alert-dialog';

            alertDialog.innerHTML = `
                <div class="alert-header">
                    <div class="alert-title">${title}</div>
                </div>
                <div class="alert-content">${message}</div>
                <div>
                    <button type="button" class="alert-confirm-button">确定</button>
                </div>
            `;

            alertOverlay.appendChild(alertDialog);
            document.body.appendChild(alertOverlay);

            // 点击确定按钮可以关闭弹窗
            const alertConfirmBtn = alertDialog.querySelector('.alert-confirm-button');
            alertConfirmBtn.addEventListener('click', () => {
                alertOverlay.remove();
                document.body.style.overflow = '';
            });

            // 点击背景可以关闭弹窗
            alertOverlay.addEventListener('click', (e) => {
                if (e.target === alertOverlay) {
                    alertOverlay.remove();
                    document.body.style.overflow = '';
                }
            });

        }
</script>
{% endblock %}

